﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using System.Xml.Linq;
using LiveWebServiceBase.ServiceUser;
using LiveWebServiceBase.ServiceUserSettings;
using LiveWebServiceBase;
using System.Web.Caching;
using System.Collections.Concurrent;
using LiveWebServiceBase.ReturnedTypes;
using System.Reflection;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace LiveWebService.PSW
{
    public class PowerShellWorker : IDisposable
    {
        #region Public Variables
        public DateTime LastAccessTime;
        #endregion

        #region Private varibles
        private static object objLock = new object();
        private RunspacePool runspacePool;
        private Runspace runspace;
        private string _sessionName, _adminname, _adminpwd;
        private int SessionCheckCount = 0;
        private DateTime NextTryTime;
        #endregion

        /// <summary>
        /// Create Exchnage Online and Office 365 sessions
        /// </summary>
        /// <param name="SessionName"></param>
        /// <param name="PSAdminName"></param>
        /// <param name="PSAdminPassword"></param>
        public PowerShellWorker(string SessionName, string PSAdminName, string PSAdminPassword)
        {
            this._sessionName = SessionName;
            this._adminname = PSAdminName;
            this._adminpwd = PSAdminPassword;

            CreateRunspace();
            CreateSession();

            CreateO365Runspace();
            CreateO365Session();
        }

        #region Destructors
        void IDisposable.Dispose()
        {
            CloseRunspace();
            GC.SuppressFinalize(this);
        }

        ~PowerShellWorker()
        {
            CloseRunspace();
        }

        public void Dispose(bool disposing)
        {
            if (disposing)
                CloseRunspace();
        }
        #endregion

        #region Private methods
        private void CreateRunspace()
        {
            //if runspace is not exist or corrupt
            if (runspacePool == null || runspacePool.RunspacePoolStateInfo.State == RunspacePoolState.Broken)
            {
                InitialSessionState session = InitialSessionState.CreateDefault();
                //session.ImportPSModule(new string[] { "MSOnline" });

                runspacePool = null;

                //runspacePool.ApartmentState = System.Threading.ApartmentState.STA;
                runspacePool = RunspaceFactory.CreateRunspacePool(session);
                //runspace.ThreadOptions = PSThreadOptions.UseCurrentThread;
                runspacePool.Open();

                //using (RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace))
                //    scriptInvoker.Invoke("Set-ExecutionPolicy RemoteSigned");
            }
        }

        private void CreateO365Runspace()
        {
            //if runspace is not exist or corrupt
            if (runspace == null || runspace.RunspaceStateInfo.State == RunspaceState.Broken)
            {
                InitialSessionState session = InitialSessionState.CreateDefault();
                session.ImportPSModule(new string[] { "MSOnline" });

                runspace = null;

                //runspacePool.ApartmentState = System.Threading.ApartmentState.STA;
                runspace = RunspaceFactory.CreateRunspace(session);
                //runspace.ThreadOptions = PSThreadOptions.UseCurrentThread;
                runspace.Open();

                //using (RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace))
                //    scriptInvoker.Invoke("Set-ExecutionPolicy RemoteSigned");
            }
        }

        private void CloseRunspace()
        {
            if (runspacePool != null)
            {
                runspacePool.Close();
                runspacePool = null;
            }
        }

        private PowerShell GetPowerShell()
        {
            PowerShell powershell = PowerShell.Create();
            powershell.RunspacePool = runspacePool;
            powershell.Streams.Error.DataAdded += new EventHandler<DataAddedEventArgs>(powersell_Error_Caused);
            return powershell;
        }

        private PowerShell GetPowerShellO365()
        {
            PowerShell powershell = PowerShell.Create();
            powershell.Runspace = runspace;
            powershell.Streams.Error.DataAdded += new EventHandler<DataAddedEventArgs>(powersell_Error_Caused);
            return powershell;
        }

        private void CheckSession()
        {
            bool IsAllRight = false;

            try
            {
                //while (powershell.InvocationStateInfo.State == PSInvocationState.Running ||
                //    powershell.InvocationStateInfo.State == PSInvocationState.Stopping)
                //    System.Threading.Thread.Sleep(1000);


                PowerShell powershell = GetPowerShell();
                powershell.Commands.AddScript("get-pssession -Name \"" + _sessionName + "\"");

                IAsyncResult powershellAsyncResult = powershell.BeginInvoke();
                PSObject pssObjects = powershell.EndInvoke(powershellAsyncResult).FirstOrDefault();

                if (pssObjects == null)
                {
                    CloseRunspace();

                    CreateRunspace();

                    CreateSession();
                }

                IsAllRight = true;
            }
            catch (Exception ex)
            {
                if (!ex.Message.Contains("The state of the current PowerShell instance is not valid for this operation"))
                {
                    CloseRunspace();
                }
                else
                    SessionCheckCount = 0;
            }
            finally
            {
                if (!IsAllRight)
                {
                    if (SessionCheckCount <= 3)
                    {
                        NextTryTime = DateTime.Now.AddSeconds(20);
                        SessionCheckCount++;

                        CreateRunspace();
                        CreateSession();

                        CheckSession();
                    }
                    else
                    {
                        if (NextTryTime > DateTime.Now)
                            throw new Exception("Session try to reconnect 3 times. Please whait a minute and check logs.");
                        else
                        {
                            SessionCheckCount = 0;
                            CheckSession();
                        }
                    }
                }
                else SessionCheckCount = 0;
            }
        }

        private void CreateSession()
        {
            PowerShell powershell = GetPowerShell();

            powershell.Commands.AddScript("$user = \"" + _adminname + "\"");
            powershell.Commands.AddScript("$password = ConvertTo-SecureString \"" + _adminpwd.Replace("$", "`$") + "\" -AsPlainText -Force");
            powershell.Commands.AddScript("$LiveCred = New-Object System.Management.Automation.PsCredential($user,$password)");
            powershell.Commands.AddScript("$rs = New-PSSession -Name " + _sessionName + " -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $LiveCred  -Authentication Basic -AllowRedirection");
            //powershell.Commands.AddScript("get-module MSOnline");
            //powershell.Commands.AddScript("Import-Module MSOnline");
            //powershell.Commands.AddScript("Connect-MsolService -Credential $LiveCred");
            //SetExecutionPolicy();

            //var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Session_New");

            //foreach (var item in ps_commands.Value)
            //{
            //    string command = item.Replace("%adminname%", this._adminname).Replace("%adminpwd%", this._adminpwd.Replace("$", "`$")).Replace("%sessionName%", this._sessionName);
            //    powershell.Commands.AddScript(command);
            //}

            powershell.Commands.AddScript("import-pssession $rs");

            IAsyncResult powershellAsyncResult = powershell.BeginInvoke();
            powershell.EndInvoke(powershellAsyncResult);
        }

        private void CreateO365Session()
        {
            PowerShell powershell = GetPowerShellO365();

            powershell.Commands.AddScript("$user = \"" + _adminname + "\"");
            powershell.Commands.AddScript("$password = ConvertTo-SecureString \"" + _adminpwd.Replace("$", "`$") + "\" -AsPlainText -Force");
            powershell.Commands.AddScript("$LiveCred = New-Object System.Management.Automation.PsCredential($user,$password)");
            powershell.Commands.AddScript("Connect-MsolService -Credential $LiveCred");

            IAsyncResult powershellAsyncResult = powershell.BeginInvoke();
            powershell.EndInvoke(powershellAsyncResult);
        }

        private void powersell_Error_Caused(object sender, DataAddedEventArgs e)
        {
            if ((sender as PSDataCollection<ErrorRecord>)[e.Index].Exception.Message.Contains("The WS-Management service cannot process the request. This user is allowed a maximum number of 3 concurrent shells"))
            {
                runspacePool.Close();
                runspacePool = null;
            }
            else
                throw (sender as PSDataCollection<ErrorRecord>)[e.Index].Exception;
        }
        #endregion

        #region Public methods
        public ICollection<PSObject> ExecuteCommand(params string[] commands)
        {
            //lock (objLock)
            {
                PowerShell powershell = GetPowerShell();

                LastAccessTime = DateTime.Now;
                CheckSession();

                foreach (string command in commands)
                    powershell.Commands.AddScript(command);

                IAsyncResult powershellAsyncResult = powershell.BeginInvoke();

                return powershell.EndInvoke(powershellAsyncResult);
            }
        }

        public ICollection<PSObject> ExecuteO365Command(params string[] commands)
        {
            //lock (objLock)
            {
                PowerShell powershell = GetPowerShellO365();

                foreach (string command in commands)
                    powershell.Commands.AddScript(command);

                IAsyncResult powershellAsyncResult = powershell.BeginInvoke();

                return powershell.EndInvoke(powershellAsyncResult);
            }
        }

        public int GetActiveSessionsCount()
        {
            return ExecuteCommand("get-pssession").Count;
        }
        #endregion
    }

    public class LivePSW
    {
        #region Private Variables
        private static object objLock = new object();

        private PowerShellWorker pswItem;

        private bool IsAllowMultipleSessions = false;

        private List<PowerShellWorker> lstPowerShellSessions;
        //private BlockingCollection<PowerShellWorker> bcPowerShellSessions;
        //private BlockingCollection<List<string>> bcTasks = new BlockingCollection<List<string>>();
        #endregion


        public delegate void AsyncCreateSession(string sessionanme, string login, string password, out Exception exception);

        void CreateSession(string sessionanme, string login, string password, out Exception exception)
        {
            exception = new Exception();
            try
            {
                lstPowerShellSessions.Add(new PowerShellWorker(sessionanme, login, password));
                //bcPowerShellSessions.Add(new PowerShellWorker(sessionanme, login, password));
            }
            catch (Exception ex)
            {
                exception = ex;
            }
        }

        void CallbackMethod(IAsyncResult ar)
        {
            AsyncResult result = (AsyncResult)ar;

            AsyncCreateSession caller = (AsyncCreateSession)result.AsyncDelegate;
            string formatString = (string)ar.AsyncState;

            Exception exception;
            caller.EndInvoke(out exception, ar);
        }

        public LivePSW(User suCurrent, string LiveID)
        {
            Account aItem = suCurrent.GetAccountByLiveID(LiveID);
            IsAllowMultipleSessions = aItem.multiplesessions;
            string currentCacheKey = suCurrent.UserName + "_" + aItem.psAdm.Split('@')[1] + "_PSW";

            lock (objLock)
            {

                if (System.Web.HttpContext.Current.Cache[currentCacheKey] == null)
                {
                    if (IsAllowMultipleSessions)
                    {
                        lstPowerShellSessions = new List<PowerShellWorker>();
                        //bcPowerShellSessions = new BlockingCollection<PowerShellWorker>();

                        for (int i = 1; i < 4; ++i)
                        {
                            try
                            {
                                if (i == 1)
                                {
                                    Exception ex = new Exception();
                                    CreateSession(currentCacheKey + i, aItem.psAdm, aItem.psAdmPwd, out ex);
                                }
                                else
                                {
                                    AsyncCreateSession amcItem = new AsyncCreateSession(this.CreateSession);

                                    Exception ex = new Exception();
                                    IAsyncResult result = amcItem.BeginInvoke(currentCacheKey + i, aItem.psAdm, aItem.psAdmPwd, out ex, new AsyncCallback(CallbackMethod), null);
                                }
                            }
                            catch (Exception ex)
                            {
                                if (ex.Message.Contains("The WS-Management service cannot process the request. This user is allowed a maximum number of 3 concurrent shells"))
                                    break;
                            }
                        }

                        System.Web.HttpContext.Current.Cache.Insert(currentCacheKey, lstPowerShellSessions, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(30), CacheItemPriority.NotRemovable, null);
                        //System.Web.HttpContext.Current.Cache.Insert(currentCacheKey, bcPowerShellSessions, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(30), CacheItemPriority.NotRemovable, null);
                    }
                    else
                    {
                        pswItem = new PowerShellWorker(currentCacheKey, aItem.psAdm, aItem.psAdmPwd);
                        System.Web.HttpContext.Current.Cache.Insert(currentCacheKey, pswItem, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(20), CacheItemPriority.NotRemovable, null);
                    }
                }
                else
                {
                    if (IsAllowMultipleSessions)
                    {
                        lstPowerShellSessions = (List<PowerShellWorker>)System.Web.HttpContext.Current.Cache[currentCacheKey];
                        //bcPowerShellSessions = (BlockingCollection<PowerShellWorker>)System.Web.HttpContext.Current.Cache[currentCacheKey];
                    }
                    else
                    {
                        pswItem = (PowerShellWorker)System.Web.HttpContext.Current.Cache[currentCacheKey];
                    }
                }
            }
        }

        public LivePSW(string sessionName, string adminName, string adminPassword)
        {
            pswItem = new PowerShellWorker(sessionName, adminName, adminPassword);
        }

        #region Public methods
        //public void SetExecutionPolicy()
        //{
        //    powershell.Commands.Clear();
        //    powershell.Commands.AddScript("Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned");
        //    powershell.Invoke();
        //}

        public void ImpersonateUser(string userid, string rolename)
        {
            ICollection<string> colCommands = new Collection<string>();

            colCommands.Add("New-ManagementRoleAssignment -Name " + rolename + " -Role ApplicationImpersonation -User " + userid);

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void CreateNewMailbox(string name, string emailAddress, int forceChangePassword, string password, string displayName, string firstName, string lastName)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Mailbox_Create");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%password%", password.Replace("$", "`$")).Replace("%name%", name).Replace("%emailAddress%", emailAddress).Replace("%forceChangePassword%", forceChangePassword.ToString()).Replace("%displayName%", displayName).Replace("%firstName%", firstName).Replace("%lastName%", lastName);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void AssignLicenses(string accountName, string usagelocation, string licenses)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Assign_Licenses");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%UserPrincipalName%", accountName).Replace("%usagelocation%", usagelocation).Replace("%licenses%", licenses);
                colCommands.Add(command);
            }

            ExecuteO365PSCommands(colCommands.ToArray());
        }

        public void SetMailboxParameters(string emailAddress, string parameters)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Mailbox_Set");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%emailAddress%", emailAddress).Replace("%parameters%", parameters);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void CreateNewGroup(string name)
        {
            CreateNewGroup(name, "");
        }

        public void CreateNewGroup(string name, string parameters)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Group_Create");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%name%", name).Replace("%parameters%", parameters);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void AddMemberToGroup(string groupName, string emailAddressm)
        {
            AddMemberToGroup(groupName, emailAddressm, "");
        }

        public void AddMemberToGroup(string groupName, string emailAddress, string parameters)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Group_AddMember");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%groupName%", groupName).Replace("%emailAddress%", emailAddress).Replace("%parameters%", parameters);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void CreateDynamicGroup(string groupName, string recipients, string parameters)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_DGroup_Create");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%groupName%", groupName).Replace("%recipients%", recipients).Replace("%parameters%", parameters);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void SetGroupParameters(string groupName, string parameters)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Group_Set");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%groupName%", groupName).Replace("%parameters%", parameters);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void SetDynamicGroupParameters(string groupName, string parameters)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_DGroup_Set");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%groupName%", groupName).Replace("%parameters%", parameters);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void SetMailboxAlias(string LiveID, string Emails, bool useExisting)
        {
            ICollection<string> colCommands = new Collection<string>();

            KeyValuePair<string, List<string>> ps_commands;

            if (useExisting)
                ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Mailbox_AddAlias_UseExisting");
            else
                ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Mailbox_AddAlias_Overwrite");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%LiveID%", LiveID).Replace("%NewEmails%", Emails);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void RemoveMailboxAlias(string LiveID, string Emails)
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Mailbox_RemoveAlias");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%LiveID%", LiveID).Replace("%Emails%", Emails);
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public void ResetPassword(string LiveID, string password, bool forceChangePwd)
        {
            string ResetPasswordOnNextLogon = string.Empty;

            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Mailbox_ResetPassword");

            foreach (var item in ps_commands.Value)
            {
                string command = item.Replace("%LiveID%", LiveID).Replace("%password%", password.Replace("$", "`$")).Replace("%resetpassword%", (forceChangePwd) ? "1" : "0");
                colCommands.Add(command);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }

        public List<PSResult> ExecutePowerShellCommand(string command)
        {
            return ExecuteEXOPSCommands(command);
        }

        public List<PSResult> ExecutePowerShellCommand(List<string> command)
        {
            return ExecuteEXOPSCommands(command.ToArray());
        }

        public void CloseSession()
        {
            ICollection<string> colCommands = new Collection<string>();

            var ps_commands = Settings.PSVariables.PSCommands.SingleOrDefault(le => le.Key == "PS_Session_Close");

            foreach (var item in ps_commands.Value)
            {
                colCommands.Add(item);
            }

            ExecuteEXOPSCommands(colCommands.ToArray());
        }
        #endregion

        #region Private methods
        private List<PSResult> ProceedResult(ICollection<PSObject> returnObjects)
        {
            List<PSResult> result = new List<PSResult>();

            foreach (PSObject psoItem in returnObjects)
            {
                PSResult psrItem = new PSResult();

                if (psoItem.ImmediateBaseObject.GetType() == typeof(PSCustomObject))
                {
                    foreach (PSMemberInfo MemberInfo in psoItem.Members)
                    {
                        if (MemberInfo.MemberType != PSMemberTypes.Method && MemberInfo.MemberType != PSMemberTypes.Methods && MemberInfo.Value != null)
                        {
                            psrItem.Properties.Add(new PropertyItem(MemberInfo.Name, MemberInfo.Value.ToString()));
                        }
                    }
                }
                else if (psoItem.ImmediateBaseObject.GetType() == typeof(int))
                    psrItem.IntValue = Convert.ToInt32(psoItem.ImmediateBaseObject);
                else if (psoItem.ImmediateBaseObject.GetType() == typeof(string))
                    psrItem.StringValue = psoItem.ImmediateBaseObject.ToString();
                else if (psoItem.ImmediateBaseObject.GetType() == typeof(PSSession))
                {
                    Type myType = psoItem.ImmediateBaseObject.GetType();
                    IList<PropertyInfo> props = new List<PropertyInfo>(myType.GetProperties());

                    foreach (PropertyInfo prop in props)
                    {
                        object propValue = prop.GetValue(psoItem.ImmediateBaseObject, null);
                        psrItem.Properties.Add(new PropertyItem(prop.Name, propValue.ToString()));
                    }
                }

                if (psrItem != null)
                    result.Add(psrItem);
            }

            return result;
        }

        private List<PSResult> ExecuteEXOPSCommands(params string[] commands)
        {
            if (IsAllowMultipleSessions)
            {
                Random randItem = new Random();
                int selectedIndex = randItem.Next(0, lstPowerShellSessions.Count - 1);
                PowerShellWorker psItem = lstPowerShellSessions[selectedIndex];
                return ProceedResult(psItem.ExecuteCommand(commands));
            }
            else
            {
                return ProceedResult(pswItem.ExecuteCommand(commands.ToArray()));
            }
        }

        private List<PSResult> ExecuteO365PSCommands(params string[] commands)
        {
            if (IsAllowMultipleSessions)
            {
                Random randItem = new Random();
                int selectedIndex = randItem.Next(0, lstPowerShellSessions.Count - 1);
                PowerShellWorker psItem = lstPowerShellSessions[selectedIndex];
                return ProceedResult(psItem.ExecuteO365Command(commands));
            }
            else
            {
                return ProceedResult(pswItem.ExecuteO365Command(commands.ToArray()));
            }
        }

        #endregion
    }
}
